/**
 *       Copyright (c) 2012 Lukas Zaruba
 *
 *   This file is part of Robot Playground.
 *
 *   Robot Playground is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Robot Playground is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with Robot Playground. If not, see <http://www.gnu.org/licenses/>.
 */
package net.zarubsys.robotplayground.core.modules;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.zarubsys.robotplayground.core.device.IDevice;
import net.zarubsys.robotplayground.core.functions.FunctionDescriptor;
import net.zarubsys.robotplayground.core.functions.Parameter;
import net.zarubsys.robotplayground.core.modules.annotations.Function;
import net.zarubsys.robotplayground.core.modules.annotations.Param;

/**
 * AbstractModule
 * 
 * Subclass this class to get a basic implementation of your module.
 * This class takes care about the dispatching of function calls.
 * 
 * Annotate functions you want to publish from the subclass by {@link Function} annotation
 * and all of its parameters by {@link Param} annotation. Provide description in the function
 * annotation and param name in the Param annotation. Calls to your functions can be then
 * processed.
 * 
 * You can use {@link #registerFunction(FunctionDescriptor, Method, Object)} method to register
 * a function without any annotations.
 *
 * @author Lukas Zaruba, lukas.zaruba@gmail.com
 */
public abstract class AbstractModule<D extends IDevice, C extends IModuleConfiguration> implements IModule<D, C> {
	
	public static final String RETURN_PARAM_NAME = "return"; 
	
	private final Map<String, FunctionDescriptor> functionsCache = new HashMap<>();
	private final Map<String, Method> methodsCache = new HashMap<>();
	private final Map<String, Object> invocationObjects = new HashMap<>();
	
	public AbstractModule() {
		loadCallableMethods();
	}

	private void loadCallableMethods() {
		Method[] methods = this.getClass().getMethods();
		for (Method m : methods) {
			Function functionAnnotation = m.getAnnotation(Function.class);
			if (functionAnnotation == null) continue;
			
			Class<?>[] parameterTypes = m.getParameterTypes();
			Annotation[][] parameterAnnotations = m.getParameterAnnotations();
			
			List<Parameter> parameters = new ArrayList<>(parameterTypes.length);
			String methodName = m.getName();
			O:for (int i = 0; i < parameterTypes.length; i++) {
				Class<?> type = parameterTypes[i];
				Annotation[] annotations = parameterAnnotations[i];
				for (Annotation a : annotations) {
					if (!a.annotationType().equals(Param.class)) continue;
					Param p = (Param) a; // we get here only if a is instance of Param
					String name = p.name();
					if (RETURN_PARAM_NAME.equals(name)) throw new IllegalStateException("Method " + methodName + " of class " + 
							getId() + " has paramter named " + RETURN_PARAM_NAME + " this one is reserved!");
					parameters.add(new Parameter(name, type, p.nullable()));
					continue O;
				}
				// if we get here it means there is missing annotation on the parameter
				throw new IllegalStateException("Method '" + methodName + "' of class '" + this.getClass().getName() + "' has unannotated paramter no." + i + 
						". @Param annotation is obligatory on each paramter!");
			}
			
			Class<?> returnType = m.getReturnType();
			Parameter returnParameter = returnType == null ? null : new Parameter(RETURN_PARAM_NAME, returnType, false);
			
			FunctionDescriptor fd = new FunctionDescriptor(methodName, parameters, returnParameter, functionAnnotation.description());
			functionsCache.put(methodName, fd);
			methodsCache.put(methodName, m);
			invocationObjects.put(methodName, this);
		}
	}
	
	@Override
	public Object callFunction(String functionName, List<Object> params) {
		FunctionDescriptor function = functionsCache.get(functionName);
		if (function == null) {
			throw new IllegalArgumentException("There is no function for name '" + functionName + "' in module '" + getId() + "'!");
		}
		
		List<Parameter> requiredParameters = function.getParameters();
		if (requiredParameters.size() != params.size()) {
			throw new IllegalArgumentException("Count of passed parameters doesn't match requested params of function '" + 
				functionName + "' in module '" + getId() + "'. Expected " + requiredParameters.size() + " got " + params.size());
		}
		
		for (int i = 0; i < params.size(); i++) {
			Object object = params.get(i);
			Parameter requiredParameter = requiredParameters.get(i);
			if (!requiredParameter.isCompliant(object)) {
				throw new IllegalArgumentException("Value for paramter no." + i + " (" + requiredParameter + ") of function '" +
					functionName + "' in module '" + getId() + "' is not compliant! Value: " + (object != null ? object.getClass().getName() + ":" : "") + object + ", expected: " + requiredParameter.getType());
			}
		}
		
		try {
			return methodsCache.get(functionName).invoke(invocationObjects.get(functionName), params.toArray());
		} catch (IllegalAccessException | IllegalArgumentException
				| InvocationTargetException e) {
			throw new IllegalArgumentException("Error while invoking function '" + functionName + "' in module '" + getId() + "'.", e);
		}
	}
	
	@Override
	public Set<FunctionDescriptor> getAllFunctions() {
		return new HashSet<>(functionsCache.values());
	}
	
	/**
	 * Use this method to register function without using an annotation.
	 * @param fd function descriptor of the function
	 * @param m method which will be called using the reflection
	 * @param invocationObject object which will be used for the invocation.
	 * This object must be an instance of object that contains the method provided.
	 */
	public void registerFunction(FunctionDescriptor fd, Method m, Object invocationObject) {
		if (fd == null || m == null || invocationObject == null) throw new IllegalArgumentException("All paramters are required!!!");
		
		String functionName = fd.getName();
		if (functionsCache.containsKey(functionName)) {
			throw new IllegalArgumentException("There already is function '" + functionName + " registered in the module '" + getId() + "'");
		}
		functionsCache.put(functionName, fd);
		methodsCache.put(functionName, m);
		invocationObjects.put(functionName, invocationObject);
	}

}

